﻿using System;
using System.Net;
using System.Windows;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Text;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Phone.Net.NetworkInformation;
using System.Globalization;
using System.IO;

namespace UPnP
{
    public class Service
    {
        private string Address;
        private string ServiceType;

        // Every Action needs to have an instance of this information, which encapsulates all the data
        // necessary to make a call
        public struct ActionInfo
        {
            public string name;
            public string[] argnames;
            public int outargs;
        };

        // Every Action that has any OUT arguments needs to have a type inherited from this one, containing
        // each OUT argument as a member
        public class ActionResult
        {
            public Exception Error;             // Stores last error returned

            // Override this to convert an array of strings into the desired type
            public virtual void Fill(string[] rawdata)
            {
                Debug.Assert(rawdata.Length == 0);      // will fire if failed to override for non-empty results
            }


            // Helper methods
            //
            protected bool ParseBool(string item)
            {
                return item == "1";
            }
        };

        protected Service(string uri, string type)
        {
            this.Address = uri;
            this.ServiceType = type;
        }

        public async Task<ActionResult> Action_Async(ActionInfo actionInfo, object[] args, ActionResult actionResult)
        {
            actionResult.Error = null;

            try
            {
                StringBuilder soap = new StringBuilder("<u:" + actionInfo.name + " xmlns:u=\"" + ServiceType + "\">");
                int currentarg = 0;

                if (args != null)
                {
                    foreach (var arg in args)
                    {
                        soap.AppendFormat("<{0}>{1}</{0}>", actionInfo.argnames[currentarg++], FormatArg(arg));
                    }
                }
                soap.AppendFormat("</u:{0}>", actionInfo.name);

                var result = await SOAPRequest_Async(new Uri(Address), soap.ToString(), actionInfo.name);

                // TODO handle errors from services here

                if (actionInfo.outargs != 0)
                {
                    // Children of u:<VERB>Response are the result we want
                    XName xn = XName.Get(actionInfo.name + "Response", ServiceType);

                    var items = from item in result.Descendants(xn).Descendants()
                                select (string)item.Value;

                    // Check that we have at least the expected number of results
                    if (items.Count() < actionInfo.outargs)
                        throw new ArgumentException();

                    actionResult.Fill(items.ToArray());
                }
            }
            catch (Exception ex)
            {
                actionResult.Error = ex;
            }

            return actionResult;
        }

        private async Task<XElement> SOAPRequest_Async(Uri target, string soap, string action)
        {
            string req = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
            "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
            "<s:Body>" +
            soap +
            "</s:Body>" +
            "</s:Envelope>";
            HttpWebRequest r = (HttpWebRequest)HttpWebRequest.Create(target);

            // We only ever need to do this over non-cellular networks
            r.SetNetworkPreference(NetworkSelectionCharacteristics.NonCellular);
            r.Method = "POST";
            byte[] b = Encoding.UTF8.GetBytes(req);
            r.Headers["SOAPACTION"] = "\"" + ServiceType + "#" + action + "\"";
            r.ContentType = "text/xml; charset=\"utf-8\"";
            var stream = await r.GetRequestStreamAsync();
            stream.Write(b, 0, b.Length);
            stream.Close();                 // else will get "Not Supported"

            WebResponse resp;
            try
            {
                resp = await r.GetResponseAsync();
            }
            catch (System.Net.WebException ex)
            {
                throw ConvertException(ex);
            }

            var st = resp.GetResponseStream();
            st.Seek(0, SeekOrigin.Begin);
            return XElement.Load(st);
        }

        public class UPnPException : Exception
        {
            public int ErrorCode
            {
                get;
                private set;
            }

            public override string Message
            {
                get
                {
                    return "UPnP Error " + ErrorCode.ToString();
                }
            }
            internal UPnPException(int error)
            {
                ErrorCode = error;
            }
        }

        // If an exception is really a UPNP error, convert it
        private Exception ConvertException(WebException ex)
        {
            WebResponse resp = ex.Response;
            string error = ex.Message;

            if (resp != null)
            {
                using (Stream respstream = resp.GetResponseStream())
                {
                    respstream.Seek(0, SeekOrigin.Begin);
                    TextReader reader = new StreamReader(respstream, Encoding.UTF8);
                    error = reader.ReadToEnd();
                    // This can be a UPnP error
                    // TODO actually parse the XML
                    int errstart = error.IndexOf("<errorCode>");
                    int errend = error.IndexOf("</errorCode>");
                    if ((errstart != -1) && (errend != -1))
                    {
                        error = "UPnP ERROR:" + error.Substring(errstart + 11, errend - errstart - 11);
                        return new UPnPException(int.Parse(error.Substring(errstart + 11, errend - errstart - 11)));
                    }
                }
            }
            return ex;
        }

        private string FormatArg(object obj)
        {
            string result;

            if (obj == null)
                result = "";
            else if (obj is string)
                result = UPnP.Utility.XmlEscape(obj.ToString());
            else if (obj is bool)
                result = (bool)(obj) ? "1" : "0";
            else
                result = Convert.ChangeType(obj, obj.GetType(), CultureInfo.InvariantCulture).ToString();

            return result;
        }

    }

}
